﻿#define SZW_LOADASSTREAM //getExtractor() func load using FileStream instead of FileName

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
#if !NETFX_CORE
using SevenZip;
#endif
using System.IO;
using System.Text.RegularExpressions;
using Cofe.Core;
using System.Threading;
using Cofe.Core.Utils;
using CofeActions = Cofe.Core.Actions;
using Cofe.IO.Interfaces;
using System.Threading.Tasks;
using System.ComponentModel.Composition;


namespace Cofe.IO.Compress
{    
#if !NETFX_CORE
    [Export(typeof(ICofeService))]
    [ServicePriority(ServicePriorityAttribute.DefaultPriority_COFEIO)]
    public class SevenZipWrapper : CompressionWrapperBase, ICompressorWrapper
    {
        #region mini tuple implementation

        public class tuple<T1, T2>
        {
            public T1 Value1 { get; private set; }
            public T2 Value2 { get; private set; }
            public tuple(T1 value1, T2 value2)
            {
                Value1 = value1;
                Value2 = value2;
            }
        }

        #endregion

        #region Constructor

        public SevenZipWrapper()
        {
            //_archiveFile = archiveFile;
        }

        #endregion

        #region Static Methods


        public static OutArchiveFormat getArchiveFormat(string archiveName)
        {

            switch (PathFE.GetExtension(archiveName).ToLower())
            {
                case ".7z":
                case ".7zip": return OutArchiveFormat.SevenZip;
                case ".zip": return OutArchiveFormat.Zip;
                case ".bzip": return OutArchiveFormat.BZip2;
                case ".z":
                case ".gz":
                case ".gzip":
                    return OutArchiveFormat.GZip;
                case ".tar":
                    return OutArchiveFormat.Tar;
                default: throw new NotImplementedException();
            }
        }

        #endregion

        #region Methods

        private SevenZipExtractor getExtractor(StreamContainer container, string password = null)
        {
            if (!String.IsNullOrEmpty(password))
                return new SevenZipExtractor(container.Stream, password);
            return new SevenZipExtractor(container.Stream);
        }

        private SevenZipExtractor getExtractor(string archivePath, string password = null)
        {
            if (!String.IsNullOrEmpty(password))
                return new SevenZipExtractor(archivePath, password);
            return new SevenZipExtractor(archivePath);
        }


        private SevenZipCompressor getCompressor(OutArchiveFormat archiveFormat, string password = null,
            IProgress<CofeActions.ProgressEventArgs> progress = null)
        {
            SevenZipCompressor compressor = new SevenZipCompressor();
            compressor.DirectoryStructure = true;
            compressor.ArchiveFormat = archiveFormat;
            compressor.CompressionMode = CompressionMode.Append;

            if (progress != null)
            {
                string workingFile = null;
                compressor.Compressing += (o, e) =>
                    {
                        var args = new CofeActions.ProgressEventArgs(e.PercentDone, 100, workingFile);
                        progress.Report(args);
                        if (args.Cancel)
                            e.Cancel = true;
                    };

                compressor.FileCompressionStarted += (o, e) =>
                    {
                        workingFile = e.FileName;

                        var args = new CofeActions.ProgressEventArgs(e.PercentDone, 100, workingFile);
                        progress.Report(args);
                        if (args.Cancel)
                            e.Cancel = true;
                    };
            }

            return compressor;
        }


        private IEnumerable<tuple<int, string>> lookup(SevenZipExtractor extractor, string pathOrMask)
        {
            foreach (ArchiveFileInfo afi in extractor.ArchiveFileData)
                if (PathFE.MatchFileMask(afi.FileName, pathOrMask))
                {
                    yield return new tuple<int, string>(afi.Index, afi.FileName);
                }
        }

        protected override IEnumerable<List<PropertyPair>> list(StreamContainer container, string pattern, bool listSubdir = false)
        {
            foreach (var afi in listCore(container, pattern, listSubdir))
            {
                    var retVal = new List<PropertyPair>()
                    {
                        PropertyPair.FromValue(CofeProperties.DefaultInterfaceType, 
                            afi.IsDirectory ? typeof(IDirectoryInfo) : typeof(IFileInfo)),
                        PropertyPair.FromValue(FileSystemInfoProperties.RelativePath, afi.FileName),
                        PropertyPair.FromValue(CofeProperties.Name, PathFE.GetFileName(afi.FileName))
                        
                    };

                    if (!(afi.IsDirectory))
                        retVal.Add(PropertyPair.FromValue(CofeItemProperties.Length, (Int64)afi.Size));

                    yield return retVal;
            }
        }

        /// <summary>
        /// List entries that match the filterStr, the filterStr must be in format of RegexPatterns.SevenZipListPattern.
        /// </summary>
        /// <param name="filterStr"></param>
        /// <returns></returns>
        internal IEnumerable<ArchiveFileInfo> listCore(StreamContainer container, string pattern, bool listSubdir)
        {
            if (pattern.IndexOf("?<name>") == -1)
                throw new ArgumentException("");
            if (pattern.IndexOf("?<trail>") == -1)
                throw new ArgumentException("");
            if (pattern.IndexOf("?<parent>") == -1)
                throw new ArgumentException("");

            List<string> returnedPathList = new List<string>();


            lock (_lockObject)
            {
                List<ArchiveFileInfo> afiList;

                try
                {
                    using (SevenZipExtractor extractor = getExtractor(container))
                    {
                        afiList = new List<ArchiveFileInfo>(extractor.ArchiveFileData);
                    }
                }
                catch { afiList = new List<ArchiveFileInfo>(); }

                foreach (ArchiveFileInfo afi in afiList)
                {
                    Match match = new Regex(pattern).Match(afi.FileName);
                    if (match.Success)
                    {
                        string parent = match.Groups["parent"].Value;
                        string name = match.Groups["name"].Value;
                        string trail = match.Groups["trail"].Value;

                        if (!afi.IsDirectory && String.IsNullOrEmpty(trail))
                        {
                            if (returnedPathList.IndexOf(afi.FileName.ToLower()) == -1)
                            {
                                yield return afi;
                                returnedPathList.Add(afi.FileName.ToLower());
                            }
                        }
                        else if (!String.IsNullOrEmpty(trail) || listSubdir)
                        {
                            string dirName = PathFE.Combine(parent, name);
                            if (returnedPathList.IndexOf(dirName.ToLower()) == -1)
                            {
                                yield return new ArchiveFileInfo()
                                {
                                    FileName = dirName,
                                    IsDirectory = true
                                };
                                returnedPathList.Add(dirName.ToLower());
                            }
                        }
                    }
                }
            }
        }


        public IEnumerable<ArchiveFileInfo> listSimple(StreamContainer container, string relativePathOrMask)
        {
            lock (_lockObject)
                using (SevenZipExtractor extractor = getExtractor(container))
                    lock (extractor)
                    {
                        foreach (ArchiveFileInfo afi in extractor.ArchiveFileData)
                        {
                            if (PathFE.MatchFileMask(afi.FileName, relativePathOrMask, true))
                                yield return afi;

                        }
                    }
        }



        public override Task<bool> TestAsync(StreamContainer container)
        {            
            return Task.FromResult<bool>(getExtractor(container, null).Check());
        }

        protected override bool exists(StreamContainer container, string pathOrMask, bool isFolder)
        {
            lock (_lockObject)
                using (SevenZipExtractor extractor = getExtractor(container))
                {
                    foreach (ArchiveFileInfo afi in extractor.ArchiveFileData)
                    {
                        if (PathFE.MatchFileMask(afi.FileName, pathOrMask) && (afi.IsDirectory == isFolder))
                            return true;
                        else if (afi.FileName.StartsWith(pathOrMask, StringComparison.InvariantCultureIgnoreCase))
                            return true;
                    }
                }
            return false;
        }


        #region Extract


        private void extract(SevenZipExtractor extractor, string mask, string toDir, string password)
        {
            if (extractor.FilesCount == 0)
                return;

            if (!extractor.IsSolid)
            {
                ExtractFileCallback callBack = (t) =>
                {
                    if (PathFE.MatchFileMask(t.ArchiveFileInfo.FileName, mask))
                    {
                        if (t.Reason == ExtractFileCallbackReason.Start)
                        {
                            string outputFileName = PathFE.Combine(toDir, t.ArchiveFileInfo.FileName);
                            string outputDir = PathFE.GetDirectoryName(outputFileName);

                            if (!Directory.Exists(outputDir))
                                Directory.CreateDirectory(outputDir);

                            if (t.ArchiveFileInfo.IsDirectory)
                            {
                                if (!Directory.Exists(outputFileName))
                                    Directory.CreateDirectory(outputFileName);
                            }
                            else
                            {
                                if (File.Exists(outputFileName))
                                    File.Delete(outputFileName);

                                t.ExtractToFile = outputFileName;
                            }
                        }
                    }
                };

                extractor.ExtractFiles(callBack);
            }
            else
            {
                List<int> indexes = new List<int>();
                foreach (var afi in extractor.ArchiveFileData)
                    if (PathFE.MatchFileMask(afi.FileName, mask))
                        indexes.Add(afi.Index);
                extractor.ExtractFiles(toDir, indexes.ToArray());
            }

        }

        protected override bool extractOne(StreamContainer container,  string fileName, string password, Stream outputStream)
        {
            using (SevenZipExtractor extractor = getExtractor(container, password))
                foreach (var afi in extractor.ArchiveFileData)
                    if (afi.FileName.Equals(fileName, StringComparison.InvariantCultureIgnoreCase))
                    {
                        extractor.ExtractFile(afi.Index, outputStream);
                        return true;
                    }
            return false;
        }

        public void extract(StreamContainer container, string mask, string toDir, string password,
            EventHandler<FileInfoEventArgs> onExtractionStarted,
            EventHandler<FileInfoEventArgs> onExtractionEnded,
            EventHandler<ProgressEventArgs> onExtracting)
        {
            using (SevenZipExtractor extractor = getExtractor(container, password))
            {
                if (onExtractionStarted != null) extractor.FileExtractionStarted += onExtractionStarted;
                if (onExtractionEnded != null) extractor.FileExtractionStarted += onExtractionEnded;
                if (onExtracting != null) extractor.Extracting += onExtracting;

                extract(extractor, mask, toDir, password);

                if (onExtractionStarted != null) extractor.FileExtractionStarted -= onExtractionStarted;
                if (onExtractionEnded != null) extractor.FileExtractionStarted -= onExtractionEnded;
                if (onExtracting != null) extractor.Extracting -= onExtracting;


            }
        }

        public void extract(StreamContainer container, string mask, string toDir, string password)
        {
            extract(container, mask, toDir, password, null, null, null);
        }
        #endregion

        #region Compress

        public void CompressMultiple(string archivePath, Dictionary<string, Stream> streamDic,
            IProgress<CofeActions.ProgressEventArgs> progress = null)
        {
            OutArchiveFormat archiveFormat = SevenZipWrapper.getArchiveFormat(archivePath);

            foreach (var key in streamDic.Keys)
                Delete(archivePath, key);

            SevenZipCompressor compressor = getCompressor(archiveFormat, null, progress);            
            lock (_lockObject)
            {                
                compressor.CompressStreamDictionary(streamDic, archivePath);
            }
        }


        public void compressOne(string archivePath, string fileName, Stream fileStream)
        {
            CompressMultiple(archivePath, new Dictionary<string, Stream>() { { fileName, fileStream } });
        }

        public bool CompressOne(StreamContainer container, string fileName, Stream fileStream)
        {
            return CompressMultiple(container, new Dictionary<string, Stream>() { { fileName, fileStream } });
        }


     
        protected override bool compressMultiple(StreamContainer container, 
            Dictionary<string, Stream> streamDic, IProgress<CofeActions.ProgressEventArgs> progress = null)
        {
            var arcFormat = getArchiveFormat("abc." + container.Type);
            SevenZipCompressor compressor = getCompressor(arcFormat, null, progress);

           //SevenZipSharp crash when compressor.CompressStreamDictionary() is used, 
           //if Compression mode is Append (Create is fine).
            //lock (_lockObject)
            //    compressor.CompressStreamDictionary(streamDic, container.Stream);

            string tempFile = null;
            lock (_lockObject)
            {

                using (var tempStream = StreamUtils.NewTempStream(out tempFile, container.Type))
                    StreamUtils.CopyStream(container.Stream, tempStream, true, true, false);
                
                CompressMultiple(tempFile, streamDic, progress);

                using (var tempStream = new FileStream(tempFile, FileMode.Open))
                    StreamUtils.CopyStream(tempStream, container.Stream, false, true, true);
            }

            return true;
        }

        #endregion

        #region Delete

        public void Delete(string archivePath, string delPathOrMask)
        {
            OutArchiveFormat archiveFormat = SevenZipWrapper.getArchiveFormat(archivePath);
            Dictionary<int, string> fileDictionary = new Dictionary<int, string>();

            foreach (var foundItem in lookup(getExtractor(archivePath, null), delPathOrMask))
                fileDictionary.Add(foundItem.Value1, null);

            if (fileDictionary.Count > 0)
            {
                lock (_lockObject)
                {
                    SevenZipCompressor compressor = getCompressor(archiveFormat);
                    compressor.ModifyArchive(archivePath, fileDictionary);
                }
            }
        }

        protected override bool delete(StreamContainer container, string delPathOrMask)
        {
            OutArchiveFormat archiveFormat = SevenZipWrapper.getArchiveFormat("aaa." + container.Type);
            Dictionary<int, string> fileDictionary = new Dictionary<int, string>();

            foreach (var foundItem in lookup(getExtractor(container, null), delPathOrMask))
                fileDictionary.Add(foundItem.Value1, null);

            if (fileDictionary.Count > 0)
            {
                string tempFile;       
                lock (_lockObject)
                {
                    using (var tempStream = StreamUtils.NewTempStream(out tempFile, container.Type))
                        StreamUtils.CopyStream(container.Stream, tempStream, true, true, false);

                    Delete(tempFile, delPathOrMask);

                    using (var tempStream = new FileStream(tempFile, FileMode.Open))
                        StreamUtils.CopyStream(tempStream, container.Stream, false, true, true);
                }

                //SevenZipSharp crash when compressor.ModifyArchive() is used, 
                //if Compression mode is Append (Create is fine).
                //lock (_lockObject)
                //{
                //    SevenZipCompressor compressor = getCompressor(archiveFormat);
                //    compressor.ModifyArchive(archivePath, fileDictionary);
                //}
            }

            return true;
        }

        #endregion


        public override bool IsArchive(string fileName)
        {
            return CompressionUtils.IsArchive(fileName);
        }

        public override byte[] GetEmptyArchiveBytes(string type)
        {
            switch (type)
            {
                case "zip": return CompressionUtils.EmptyZip;
                case "7z": return CompressionUtils.Empty7z;
            }
            return null;
        }

        #endregion

        #region Data

        //private StreamContainer _archiveFile;        
        private static object _lockObject = new object();
        private static object _globalLockObject = new object();

        #endregion

        #region Public Properties

        //public IInternalFileInfoExA ArchiveFile { get { return _archiveFile; } }


        #endregion




    }
#endif
}
